#from __future__ import print_function

from django.db import models
#from django.contrib.auth.models import User
import sys

from string import lower

import logging
logger = logging.getLogger('psd.register.models')


GENDERS = (
    ('M', 'male'),
    ('F', 'female'),
    ('TM', 'trans male'),
    ('TF', 'trans female'),
    ('Q', 'genderqueer'),
)

LOCATIONS = (
    ('EB', 'East Bay'),
    ('SB', 'South Bay'),
    ('SF', 'San Francisco'),
    ('NB', 'North Bay'),
    ('PN', 'Peninsula'),
    ('SE', 'somewhere else'),
)

KINKINESSES = (
    ('K', 'kinky only'),
    ('NK', 'non-kinky only'),
    ('EI', 'either is fine'),
)

def csv_to_set(s):
    """
    >>> csv_to_set('a,b')
    set(['a', 'b'])
    >>> csv_to_set('')
    set([])
    """
    return set(s.split(',') if s else [])



class Person(models.Model):
    first_name = models.CharField(max_length=30, verbose_name="First Name")
    last_name = models.CharField(max_length=30, verbose_name="Last Name")
    gender = models.CharField(max_length=20, verbose_name="Gender")
    age = models.PositiveIntegerField(verbose_name="Age")
    seeking_primary = models.BooleanField(verbose_name="Seeking Primary (Y/N)")
    kinky = models.BooleanField(verbose_name="Kinky (Y/N)")
    seek_gender = models.CharField(max_length=20, verbose_name="Genders Wanted")
    seek_age_min = models.PositiveIntegerField(verbose_name="Minimum Age Wanted")
    seek_age_max = models.PositiveIntegerField(verbose_name="Maximum Age Wanted")
    seek_kinkiness = models.CharField(max_length=2, verbose_name="Kinkiness Wanted")
    psdid = models.CharField(max_length=6, blank=True) # unique=TRUE

    def __unicode__(self):
        return self.first_name + ' ' + self.last_name

    @property
    def gender_set(self):
        res = csv_to_set(self.gender)
        return res

    @property
    def seek_gender_set(self):
        res = csv_to_set(self.seek_gender)
        return res

    def interest_score(self, other):
        if self.will_date(other):
            return 1 + self.bonus_for(other)
        else:
            return 0

    @property
    def will_date_kinky(self):
        return self.seek_kinkiness in ('K','EI')

    @property
    def will_date_nonkinky(self):
        return self.seek_kinkiness in ('NK','EI')

    def mutual(self, other):
        return self.will_date(other) and other.will_date(self)

    def will_date(self, other):
        return \
            bool(other.gender_set & self.seek_gender_set) and \
            other.age >= self.seek_age_min and \
            other.age <= self.seek_age_max and \
            (self.will_date_kinky if other.kinky else self.will_date_nonkinky)

    def bonus_for(self, other):
        bonus = 0
        if other.age >= self.seek_age_min+5 and \
           other.age <= self.seek_age_max-5:
            bonus += 2
        if self.kinky == other.kinky:
            bonus += 2
        return bonus

    def mutual_with_any(self, group):
        return any(self.mutual(o) for o in group.members)

    def isG(self,gen):
        if gen in csv_to_set(self.gender):
            return True
        else:
            return False

    def lookG(self,gen):
        gs = self.seek_gender.split(",")
        if gen in gs:
            return True
        else:
            return False

    def genderDesc(self):
        s = ""
        if self.isG( "M" ):
            s += "man/"
        if self.isG( "TM" ):
            s += "trans man/"
        if self.isG( "F" ):
            s += "woman/"
        if self.isG( "TF" ):
            s += "trans woman/"
        if s != "":
            s = s[0:-1]
        if self.isG( "Q" ):
            s += " gender queer"
        if s == "":
            s = " without gender ID"

        return s.strip()

    def genderLookDesc(self):
        s = ""
        if self.lookG( "M" ):
            s += "men/"
        if self.lookG( "TM" ):
            s += "trans men/"
        if self.lookG( "F" ):
            s += "women/"
        if self.lookG( "TF" ):
            s += "trans women/"
        if self.lookG( "Q" ):
            s += "gender-queers/"

        if s == "":
            s = "nothing"
        else:
            s = s[0:-1]

        return s.strip()

    def miniCode( self ):
        """Short string to describe person's dating characteristics"""
        s = str(self.age) + self.gender 
        if self.kinky:
           s = s + "k"
        if self.seeking_primary:
           s = s + "p"
        s = s + "->" + str(self.seek_age_min) + "-" + str(self.seek_age_max)\
                         + self.seek_gender + lower(self.seek_kinkiness[0])
        return s#.tolower()


    def geekCode( self, sayPrimaryStatus=True ):
        s = "a " + str(self.age) + " yr old " + self.genderDesc()

        logger.debug( "geekCode called for %s" % (self.psdid, ) )

        if self.kinky:
            s += " (kinky)"
        else:
            s += " (not kinky)"

        if sayPrimaryStatus:
             if self.seeking_primary:
                s += ", interested in a primary,"
             else:
                s += ""

        s += " looking for " + str(self.seek_age_min) + "-" + str(self.seek_age_max) + " yr old"

        if self.seek_kinkiness == 'EI':
            s += " kinky or not "
        elif self.seek_kinkiness == 'NK':
            s += " not kinky "
        else:
            s += " kinky "

        s += self.genderLookDesc()

        self.geek_code = s

        return s


    def pronoun(g):
        if not isinstance(g, list):
            g = g.split(',')
        if 'Q' in g or ('M' in g and 'F' in g) or ('TM' in g and 'TF' in g): return "Zie"
        elif 'TM' in g: return "He"
        elif 'TF' in g: return "She"
        elif 'M' in g: return "He"
        elif 'F' in g: return "She"
        else: return "Zie"
        ## This last case should be impossible, but...


    def long_gender(self, plural=False):
        g = self.gender
        names = {'M': ('man', 'men'),
                 'F': ('woman', 'women'),
                 'TM': ('transman', 'transmen'),
                 'TF': ('transwoman', 'transwomen'),
                 'Q': ('genderqueer', 'genderqueers')}
        if not isinstance(g, list):
            g = g.split(',')
        ## Use sets here in case order is somehow scrambled.
        if set(g) == set(names):
            return "people of any gender" if plural else "person of all genders"
        else:
            ## Because we are bad people, we will use the fact that
            ## you can treat bools as ints with True == 1 and False == 0.
            return '/'.join(names[x][plural] for x in g)

    @property
    def fullname(self):
        return self.first_name + " " + self.last_name



class RegRecord(models.Model):
    nickname = models.CharField(max_length=30, verbose_name="Nickname")
    email = models.EmailField(verbose_name="Email")
    add_to_mailings = models.BooleanField(default=True, verbose_name="Join Our Mailing List (Y/N)")
    #previous_id = models.CharField(max_length=8, blank=True, verbose_name="Previous Poly Speed Dating ID")
    seek_groups = models.BooleanField(verbose_name="Date Groups (Y/N)")
    groups_match_all = models.BooleanField(verbose_name="All Group Members Must Match (Y/N)")
    friend_dates = models.BooleanField(verbose_name="Friend Dates (Y/N)")
    referred_by = models.CharField(max_length=30, blank=True, verbose_name="Referred By")
    pals = models.TextField(blank=True, verbose_name="Friends")
    location = models.CharField(max_length=30, blank=True, verbose_name="Location")
    wants_childcare = models.BooleanField(verbose_name="Need Childcare (Y/N)")
    children = models.TextField(blank=True, verbose_name="Children")
    comments = models.TextField(blank=True, verbose_name="Comments")
    event = models.CharField(max_length=10, blank=True)
    people = models.ManyToManyField(Person, blank=True)
    #user = models.OneToOneField(User, null=True, blank=True)
    psdid = models.CharField(max_length=6, blank=True)
    paid = models.BooleanField(default=False)
    cancelled = models.BooleanField(default=False)
    pending = models.BooleanField(default=False)
    here = models.BooleanField(default=False)
    stationary = models.BooleanField(default=False)
    is_group = models.BooleanField(default=False)
    notes = models.TextField(blank=True)
    geek_code = models.TextField(blank=True)
    matches = models.IntegerField(blank=True, null=True, default=0)
    oneway = models.IntegerField(blank=True, null=True, default=0)

    @property
    def members(self):
        return self.people.all()

    @property
    def size(self):
        return self.people.count()

    def atLoc(self,loc):
        gs = csv_to_set(self.location)
        if loc in gs:
            return True
        else:
            return False


    def long_place(self):
        p = location
        placenames = dict(LOCATIONS)
        if not isinstance(p, list):
            p = p.split(',')
        # return '/'.join( ("far away" if x == 'SE' else "in " + placenames[x]) for x in p)
        return 'somewhere'


    def miniCode( self ):
        """Short string to describe record's dating characteristics"""

        numPeeps = self.size

        if not self.is_group:
            s = self.indiv.miniCode()
        else:
            peeps = self.members
            s = peeps[0].miniCode()
            for p in peeps[1:]:
                    s += "; " + p.miniCode()

            if self.groups_match_all:
                s += "-all"

        if not self.seek_groups:
            s += ""
        elif self.groups_match_all:
            s += "-all"
        else:
            s += "-any"

        if self.friend_dates:
            s += "-F"

        if self.stationary:
            s += "(S)"

        return s
        

    def geekCode( self ):
        numPeeps = self.size

        if not self.is_group:
            s = self.indiv.geekCode( True )
        else:
            peeps = self.members
            s = "a group of " + peeps[0].geekCode(False)
            for  p in peeps[1:]:
                    s += " and " + p.geekCode(False)

            if not self.groups_match_all:
                s += ". Matching one member is fine"
            else:
                s += ". All members must match"

        s += "."

        if not self.seek_groups:
            s += "  Groups not okay."
        elif self.groups_match_all:
            s += "  Groups okay if all match."
        else:
            s += "  Individuals in groups okay."

        if self.friend_dates:
            s += "  Friendship dates okay."
        else:
            s += "  No friendship dates."

        if self.stationary:
            s += "  Needs to stay in same spot."

        ss = ""
        if self.atLoc('EB'):
             ss += "the East Bay and "
        if self.atLoc('SB'):
             ss += "the South Bay and "
        if self.atLoc('NB'):
            ss += "the North Bay and "
        if self.atLoc('SF'):
            ss += "SF and "
        if self.atLoc('PN'):
            ss += "the Peninsula and "
        if self.atLoc('SE'):
            ss += "outside the bay area and "
        if ss == "":
            s += " No residence given."
        else:
            s += "  From " + ss[0:-5] + "."

        return s

#    def save( self, force_insert=False, force_update=False ):
#        self.geek_code = self.geekCode()
#        print 'Over-ride on save in RegRec to sneak in geekcode\n'
#        models.Model.save( self, force_insert, force_update )


    def hasNotes( self ):
        if self.notes == "":
           return False
        else:
           return True

    def addNote(self, note ):
        """ append note to field """
        if self.hasNotes():
            self.notes = self.notes + "; " + note
        else:
            self.notes = note
        self.save()
        
        
    def __unicode__(self):
        return "%s reg %s - %s" % (self.event, self.psdid, '+'.join(str(s) for s in self.members))

    def integrity_ok(self):
        if self.is_group:
            if self.size > 1:
                return False
        else:
            if self.size != 1:
                return False

        return True

    @property
    def indiv(self):
        assert not self.is_group
        assert self.size == 1, self
        return self.members[0]

#    def all_like(self, whom):
#        ''' Do everyone in this group like a person? '''
#        return all(p.will_date(whom) for p in self.people)
#
#    def all_liked_by(self, whom):
#        ''' Is everyone in this group liked by a person? '''
#        return all(whom.will_date(p) for p in self.people)
#
#    def any_like_all(self, other):
#        ''' Does anyone in this group like everyone in another group? '''
#        return any(other.all_liked_by(p)  for p in self.people)

    def any_match_someone(self, other):
        ''' Does anyone in this group mutually match someone in another group? '''
        return any(p.mutual_with_any(other) for p in self.members)

    def all_match_someone(self, other):
        ''' Does everyone in this group mutually match someone in another group? '''
        return all(p.mutual_with_any(other) for p in self.members)

    @property
    def location_set(self):
        return csv_to_set(self.location)

    @property
    def genders(self):
        return set(g for p in self.members for g in p.gender_set)

    @property
    def seek_genders(self):
        return set(g for p in self.members for g in p.seek_gender_set)

    def has_gender(self, g):
        return g in self.genders

    def wants_gender(self, g):
        return g in self.seek_genders

    @property
    def one_cisgender(self):
        sg = self.genders
        return (len(sg) == 1) and (sg[0] in ('M','F'))

    @property
    def only_alt_gendered(self):
        return (not self.has_gender('M')) and (not self.has_gender('F'))

    @property
    def wants_mf(self):
        return self.wants_gender('M') and self.wants_gender('F')

    def mf_gender_match(self, other):
        return (self.has_gender('M') and other.has_gender('M'))  or \
               (self.has_gender('F') and other.has_gender('F'))

    def mf_gender_cross(self, other):
        return (self.has_gender('M') and other.has_gender('F'))  or \
               (self.has_gender('F') and other.has_gender('M'))

    def location_overlap(self, other):
        return bool(self.location_set & other.location_set)

    def interest_score(self, other):
        score = 1
        if other.is_group and (not self.seek_groups):
            return 0

        if self.location_overlap(other):
            score += 2

        if self.is_group:
            if self.groups_match_all:
                if not other.is_group:
                    score += 1
                return score if self.all_match_someone(other) else 0
            else:
                return score if self.any_match_someone(other) else 0

        else: # we are an individual
            me = self.indiv
            if other.is_group:
                if self.groups_match_all:
                    if all(me.will_date(o) for o in other.members):
                        return score + \
                            min(me.bonus_for(o) for o in other.members)
                    else:
                        return 0
                else: # only need to match one of them
                    if any(me.will_date(o) for o in other.members):
                        return score + \
                            max(me.bonus_for(o) for o in other.members)
                    else:
                        return 0
            else:
                you = other.indiv
                if not me.will_date(you):
                    return 0
                if me.seeking_primary and you.seeking_primary:
                    score += 2
                return score + me.bonus_for(you)

    @property
    def is_a_male(self):
        return (not self.is_group) and self.indiv.gender == 'M'

    @property
    def treat_as_female(self):
        return not self.is_a_male


    @property
    def straightish_male(self):
        return self.is_a_male and not (self.wants_gender("M") or self.wants_gender("TM"))
    
        
    def ok_gay_match(self, other):
        """ Return true if person is not bi or the match would be a "gay" match"""
        if not self.wants_mf:
            return True
        return (self.treat_as_female and other.treat_as_female) or \
               (self.is_a_male and other.is_a_male)

  
    def ok_str_match(self, other):
       if not self.wants_mf:
            return True
       return (self.treat_as_female and other.is_a_male) or \
               (self.is_a_male and other.treat_as_female)
  

    def matrix_score(self, other, matrix_type):
        if ( matrix_type == 'gay' and not ok_gay_match( other ) ) \
           or ( matrix_type == 'str' and not ok_str_match( other ) ):
            return 0
        
        return self.interest_score(other)
    
    def all_past_dates(self):
        """ Return all past dates person has had """
        drs = DateRecord.objects.filter(psdid=self.psdid)
        drids = set(c.other_psdid for c in drs)
        
        return drids
    
    def all_additionals_old(self):
        """ Return list of all additional folks to not date"""
        from django.db import connection, transaction
        cursor = connection.cursor()

        cursor.execute("SELECT MatchA,MatchB from additionals WHERE MatchA=%s OR MatchB=%s", [self.psdid, self.psdid] )
        psdids = cursor.fetchall()
        psdids = set(c[c[0]==self.psdid] for c in psdids)
        return psdids

    def all_additionals(self):
        """ Return list of all additional folks to not date"""
        drs = BreakRecord.objects.filter(psdid=self.psdid )
        drids = set(c.other_psdid for c in drs)
        
        return drids
        
    @property
    def namestring(self):
        pnames = ','.join( c.fullname for c in self.members )
        return self.nickname + ": " + pnames
    
    
    
class MatchRecord( models.Model ):
    psdid1 = models.CharField(max_length=30, verbose_name="subject's PSDID")
    psdid2 = models.CharField(max_length=30, verbose_name="object's PSDID")
    event = models.CharField(max_length=10, blank=True)
    match = models.PositiveIntegerField(verbose_name="Likability")
    gay_ok = models.BooleanField(verbose_name="Gay Round Okay")
    str_ok = models.BooleanField(verbose_name="Straight Round Okay")
    
#    def __init__(self,event,psdid1,psdid2,match,gay_ok,str_ok):
#        self.psdid1=psdid1
#        self.psdid2=psdid2
#        self.event=event
#        self.match=match
#        self.gay_ok=gay_ok
#        self.str_ok=str_ok
        
    def __unicode__(self):
        return self.psdid1 + "-" + self.psdid2 + ": " + str(self.match) + "/" + str(self.gay_ok) + str(self.str_ok)



   
    
def print_matrix(m, no_group=False):
    for a in m.keys():
        if not no_group or (not a[1].is_group and not a[0].is_group):
           print("%d\t %s(%s) X %s(%s)\n" % ( m[a], a[0].nickname, a[0].miniCode(), a[1].nickname, a[1].miniCode(),))

def make_matrix(matrix_type):
    assert matrix_type in ('gay','str','all')
    lefts = RegRecord.objects.all()
    rights = RegRecord.objects.all()
    return dict(((l,r), l.matrix_score(r, matrix_type)) \
                for l in lefts for r in rights)


def mutualize_matrix(scores):
    for (l,r) in scores:
        scores[l,r] = min(scores[l,r], scores[r,l])

def make_sym_matrix(matrix_type):
    m = make_matrix(matrix_type)
    mutualize_matrix(m)
    return m


def write_csv(event,fh):
    fh.write("event,PSDID1,PSDID2,match,gay_ok,str_ok\n") # header
    regs = RegRecord.objects.filter(event=event)
    for l in regs:
        for r in regs:
            match = min(l.interest_score(r), r.interest_score(l))
            gay_ok = l.ok_gay_match(r) and r.ok_gay_match(l)
            str_ok = l.ok_str_match(r) and r.ok_str_match(l)
            cells = (l.event,l.psdid,r.psdid,match,int(gay_ok),int(str_ok))
            row = ','.join(str(c) for c in cells)
            fh.write(row)
            fh.write('\n')


def listMatchRecords(event, verbose=False):
    """ 
    Figure out pairs of potential dates.   For debugging
    """
    
    cntr = 0
    regs = RegRecord.objects.filter(event=event)
    ticker=1
    for l in regs:
        alladd = l.all_additionals()
        allpast = l.all_past_dates()
        if verbose:
            print "%s] Matching %s [past: %s %s]: " % (ticker, l.psdid, len(alladd), len(allpast) ),
        ticker = ticker + 1
        alladd = alladd.union( allpast )
        mymatch = 0
        oneway = 0
        regs2 = RegRecord.objects.filter(event=event)
        for r in regs2: 
                   if verbose:
                        sys.stdout.write( "+" )            
#            if not r.psdid in alladd and l.interest_score(r) > 0:
#                 oneway += 1
#                 match = min(l.interest_score(r), r.interest_score(l))
#                 if match > 0:
#                    if verbose:
#                        sys.stdout.write( "+" )
#                    cntr += 1
#                    mymatch += 1
#        l.matches = mymatch
#        l.oneway = oneway
#        l.save()
        if verbose:
            print "\n",
        
    return (cntr, len(regs) )


def updateMatchRecords(event, verbose=False):
    """ 
    Figure out pairs of potential dates.   Also store number of matches
    for each regrecord
    """
    
    MatchRecord.objects.filter( event=event ).delete()
    
    cntr = 0
    regs = RegRecord.objects.filter(event=event, cancelled=False)
    ticker=1
    recticker=0
    for l in regs:
        alladd = l.all_additionals()
        allpast = l.all_past_dates()
        if verbose:
            print "%s] Matching %s [past: %s %s]: " % (ticker, l.psdid, len(alladd), len(allpast) ),
        ticker = ticker + 1
        alladd = alladd.union( allpast )
        mymatch = 0
        oneway = 0
        regs2 = RegRecord.objects.filter(event=event, cancelled=False)
        for r in regs2:     
            recticker = recticker + 1
            if not r.psdid in alladd and l.interest_score(r) > 0:
                 oneway += 1
                 match = min(l.interest_score(r), r.interest_score(l))
                 if match > 0:
                    if verbose:
                        sys.stdout.write( "+" )
                    gay_ok = l.ok_gay_match(r) and r.ok_gay_match(l)
                    str_ok = l.ok_str_match(r) and r.ok_str_match(l)
                    mt = MatchRecord(event=event,psdid1=l.psdid, psdid2=r.psdid,match=match,gay_ok=gay_ok,str_ok=str_ok)
                    mt.save()
                    cntr += 1
                    mymatch += 1
        l.matches = mymatch
        l.oneway = oneway
        l.save()
        if verbose:
            print "\n",
        
    return (cntr, len(regs), recticker )



class Event(models.Model):
    event = models.CharField( max_length=20 )
    longname = models.CharField( max_length=40 )
    location = models.CharField( max_length= 100 )
    address = models.CharField( max_length=100 )
    locationURL = models.URLField( )
    accessdetails = models.TextField( max_length=200 )
    date = models.DateField()
    starttime = models.TimeField()
    deadlinetime = models.TimeField()
    stoptime = models.TimeField()
    
    def __unicode__(self):
        return self.event + " on " + str(self.date)
    

class DateRecord(models.Model):
    """ 
    This stores dating history.   
    
    There _should be_ pairs of entries where if psdid, other_psdid is in the 
    database, then the reverse should be as well.
    """
    friend_date = models.BooleanField(default=True, verbose_name="Date was a Friendship Date (Y/N)")
    psdid = models.CharField(max_length=6, blank=True)
    other_psdid = models.CharField(max_length=6, blank=True)
    table = models.CharField(max_length=10,blank=True)
    round = models.PositiveIntegerField(verbose_name="Dating Round")
#    event = models.ForeignKey(Event, null=True)
    event = models.CharField( max_length=20 )
    said_yes = models.NullBooleanField(default=True, verbose_name="psdid said YES to other_psdid (Y/N)", null=True )
    they_said_yes = models.NullBooleanField(default=True, verbose_name="other_psdid said YES to psdid (Y/N)", null=True )
    notes = models.TextField(blank=True,null=True)

    def is_mutual(self):
        if self.said_yes==None or self.they_said_yes==None:
             return None
        elif self.said_yes and self.they_said_yes:
             return True
        else:
             return False

    def __unicode__(self):
        if self.said_yes==None:
             ss = ""
             if self.friend_date:
                 ss = ss + " (F)"
             return "%s dating %s%s" % (self.psdid, self.other_psdid, ss, )
        else:
             ss = ""
             if self.said_yes:
                 if self.they_said_yes==None:
                     ss = "yes/?"
                 elif self.they_said_yes:
                     ss = "mutual"
                 else:
                     ss = "yes/no"
             else:
                 if self.they_said_yes==None:
                     ss = "no/?"
                 elif self.they_said_yes:
                     ss = "no/yes"
                 else:
                     ss = "no/no"

             if self.friend_date:
                 ss = ss + " (F)"
             return "%s dating %s - %s" % (self.psdid, self.other_psdid, ss)
         
         
         
class BreakRecord(models.Model):
    """ 
    This stores a hand-break (i.e., two people who should not be seated
    next to each other)   
    
    There does not need to be symmetry in these (i.e. pairs of entries where 
    if psdid, other_psdid is in the 
    database, then the reverse should be as well.)
    """
    friend_ok = models.BooleanField(default=False, verbose_name="Friendship Date Still Okay (Y/N)")
    psdid = models.CharField(max_length=6, blank=True)
    other_psdid = models.CharField(max_length=6, blank=True)
    notes = models.TextField(blank=True,null=True)

    def __unicode__(self):
        return "%s X %s - %s" % ( self.psdid, self.other_psdid, self.notes )
    

